use super::cob::*;
use super::tob::{TOB, TOBCoord};
use super::track::*;

use std::collections::HashMap;

pub const COB_ROW_SIZE: i64 = 9;
pub const COB_COL_SIZE: i64 = 13;

pub const TOB_ROW_SIZE: i64 = 4;
pub const TOB_COL_SIZE: i64 = 4;

#[derive(Clone)]
pub struct Interposer {
    cobs: HashMap<COBCoord, COB>,
    tobs: HashMap<TOBCoord, TOB>,
}

impl Interposer {
    pub fn new() -> Self {
        let mut cobs = HashMap::new();
        for row in 0..COB_ROW_SIZE {
            for col in 0..COB_COL_SIZE {
                cobs.insert(COBCoord::new(row, col), COB::new());
            }
        }

        let mut tobs = HashMap::new();
        for row in 0..TOB_ROW_SIZE {
            for col in 0..TOB_COL_SIZE {
                tobs.insert(TOBCoord::new(row, col), TOB::empty());
            }
        }
        Self { cobs, tobs }
    }
}

impl Interposer {
    pub fn get_cob(&self, coord: &COBCoord) -> Option<&COB> {
        self.cobs.get(coord)
    }

    pub fn get_tob(&self, coord: &TOBCoord) -> Option<&TOB> {
        self.tobs.get(coord)
    }
}

impl Interposer {
    pub fn adjacent_idle_tracks(&self, track: &Track) -> Vec<(Track, COBConnector)> {
        self.adjacent_tracks(track).into_iter()
            .filter(|(t, c)| -> bool {
                !c.is_connected() && self.is_idle_track(t)
            })
            .collect()
    }

    pub fn connected_tracks(&self, track: &Track) -> Vec<Track> {
        self.connected_tracks_with_from(track, None)
    }

    fn connected_tracks_with_from(&self, track: &Track, prev_track: Option<&Track>) -> Vec<Track> {
        let mut conneted_tracks = self.adjacent_tracks(track).into_iter()
            .filter(|(t, c)| -> bool {
                if let Some(prev_track) = prev_track {
                    prev_track != t && c.is_connected()
                } else {
                    c.is_connected()
                }
            })
            .map(|(t, _)| t)
            .map(|t| -> Vec<Track> {
                self.connected_tracks_with_from(&t, Some(track))
            })
            .flatten()
            .collect::<Vec<_>>();
        
        conneted_tracks.push(*track);
        conneted_tracks
    }

    fn adjacent_tracks(&self, track: &Track) -> Vec<(Track, COBConnector)> {
        match track.dir {
            TrackDirection::Horizontal => {
                // Try left and right
                let mut result = Vec::new();

                // Left
                let cob_coord = COBCoord::new(track.row, track.col - 1);
                if let Some(cob) = self.cobs.get(&cob_coord) {
                    cob.adjacent_connectors_from_right(track.index)
                        .into_iter()
                        .for_each(|connector| {
                            let track = match connector.to_dir {
                                COBDirection::Left => Track::new(
                                    track.row,
                                    track.col - 1,
                                    TrackDirection::Horizontal,
                                    connector.to_index,
                                ),
                                COBDirection::Right => unreachable!(),
                                COBDirection::Down => Track::new(
                                    track.row,
                                    track.col - 1,
                                    TrackDirection::Vertical,
                                    connector.to_index,
                                ),
                                COBDirection::Up => Track::new(
                                    track.row + 1,
                                    track.col - 1,
                                    TrackDirection::Vertical,
                                    connector.to_index,
                                ),
                            };
                            result.push((track, connector));
                        })
                }

                // Right
                let cob_coord = COBCoord::new(track.row, track.col);
                if let Some(cob) = self.cobs.get(&cob_coord) {
                    cob.adjacent_connectors_from_left(track.index)
                        .into_iter()
                        .for_each(|connector| {
                            let track = match connector.to_dir {
                                COBDirection::Right => Track::new(
                                    track.row,
                                    track.col + 1,
                                    TrackDirection::Horizontal,
                                    connector.to_index,
                                ),
                                COBDirection::Left => unreachable!(),
                                COBDirection::Down => Track::new(
                                    track.row,
                                    track.col,
                                    TrackDirection::Vertical,
                                    connector.to_index,
                                ),
                                COBDirection::Up => Track::new(
                                    track.row + 1,
                                    track.col,
                                    TrackDirection::Vertical,
                                    connector.to_index,
                                ),
                            };
                            result.push((track, connector));
                        })
                }

                result
            }
            TrackDirection::Vertical => {
                // Try Up and Down
                let mut result = Vec::new();

                // Up
                let cob_coord = COBCoord::new(track.row, track.col);
                if let Some(cob) = self.cobs.get(&cob_coord) {
                    cob.adjacent_connectors_from_down(track.index)
                        .into_iter()
                        .for_each(|connector| {
                            let track = match connector.to_dir {
                                COBDirection::Left => Track::new(
                                    track.row,
                                    track.col,
                                    TrackDirection::Horizontal,
                                    connector.to_index,
                                ),
                                COBDirection::Right => Track::new(
                                    track.row,
                                    track.col + 1,
                                    TrackDirection::Horizontal,
                                    connector.to_index,
                                ),
                                COBDirection::Down => unreachable!(),
                                COBDirection::Up => Track::new(
                                    track.row + 1,
                                    track.col,
                                    TrackDirection::Vertical,
                                    connector.to_index,
                                ),
                            };
                            result.push((track, connector));
                        })
                }

                // Down
                let cob_coord = COBCoord::new(track.row - 1, track.col);
                if let Some(cob) = self.cobs.get(&cob_coord) {
                    cob.adjacent_connectors_from_up(track.index)
                        .into_iter()
                        .for_each(|connector| {
                            let track = match connector.to_dir {
                                COBDirection::Left => Track::new(
                                    track.row - 1,
                                    track.col,
                                    TrackDirection::Horizontal,
                                    connector.to_index,
                                ),
                                COBDirection::Right => Track::new(
                                    track.row - 1,
                                    track.col + 1,
                                    TrackDirection::Horizontal,
                                    connector.to_index,
                                ),
                                COBDirection::Up => unreachable!(),
                                COBDirection::Down => Track::new(
                                    track.row - 1,
                                    track.col,
                                    TrackDirection::Vertical,
                                    connector.to_index,
                                ),
                            };
                            result.push((track, connector));
                        })
                }

                result
            }
        }
    }

    fn is_idle_track(&self, track: &Track) -> bool {
        self.adjacent_tracks(track).into_iter()
            .all(|(_, c)| !c.is_connected())
    }
}

impl Interposer {    
    pub fn bump_to_track(&self, coord: &TOBCoord, bump_index: usize) -> Option<usize> {
        let tob = self.get_tob(coord)?;
        tob.b_index_map_t_index(bump_index)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_adjacent_tracks() {
        let i = Interposer::new();

        assert_eq!(
            i.adjacent_tracks(&Track::new(3, 3, TrackDirection::Horizontal, 12))
                .len(),
            6
        );
        assert_eq!(
            i.adjacent_tracks(&Track::new(0, 3, TrackDirection::Vertical, 12))
                .len(),
            3
        );

    }

    #[test]
    fn test_adjacent_idle_tracks() {
        let i = Interposer::new();

        let res: Vec<_> = i
            .adjacent_idle_tracks(&Track::new(0, 0, TrackDirection::Horizontal, 0))
            .into_iter()
            .map(|(t, _)| t)
            .collect();
        assert_eq!(res.len(), 3);

        assert!(res.contains(&Track::new(0, 1, TrackDirection::Horizontal, 0)));
        assert!(res.contains(&Track::new(0, 0, TrackDirection::Vertical, 8)));
        assert!(res.contains(&Track::new(1, 0, TrackDirection::Vertical, 48)));

        let res: Vec<_> = i
            .adjacent_idle_tracks(&Track::new(0, 1, TrackDirection::Horizontal, 0))
            .into_iter()
            .map(|(t, _)| t)
            .collect();
        assert_eq!(res.len(), 6);

        assert!(res.contains(&Track::new(0, 0, TrackDirection::Vertical, 48)));
    }

    #[test]
    fn test_adjacent_idle_tracks3() {
        let i = Interposer::new();

        let track = Track::new(2, 2, TrackDirection::Horizontal, 0);

        let res = i.adjacent_idle_tracks(&track);
        assert_eq!(res.len(), 6);
        res[0].1.connect();
        res[1].1.connect();

        let res = i.adjacent_idle_tracks(&track);
        assert_eq!(res.len(), 4);

    }
}

